<?php

namespace paper\view;

use paper\App;
use paper\exception\BaseException;
use paper\exception\TemplateException;

class Engine
{
    private string $leftLimit = '';
    private string $rightLimit = '';
    private string $begin = '';
    private string $end = '';

    private array $vars = [];
    private string $templateContent = '';
    private string $templateFile = '';
    private string $compileFile = '';


    protected $sampleTags = [];
    protected $tags = [];
    private $matches;
    protected $position = 0;

    function __construct(App $app)
    {
        $this->begin      = preg_quote("{", "#");
        $this->end        = preg_quote("}", "#");
        $this->leftLimit  = '(?:\<\!\-\-)?' . $this->begin;
        $this->rightLimit = $this->end . '(?:\-\-\>)?';


    }

    /**
     * @param null $template
     * @throws BaseException
     */
    public function view($template = null)
    {
        $this->load($template);
    }

    /**
     * @param $str
     * @throws BaseException
     */
    public function template($str)
    {
        $this->load($str);
    }


    /**
     * @param $template
     * @throws BaseException
     */
    public function load($template)
    {

        $this->templateFile = $this->parseTemplatePath($template);
        $this->compileFile  = $this->parseCachePath($template);

        if (!file_exists($this->templateFile)) {
            throw new TemplateException("模板不存在" . $this->templateFile);
        }


        if (!$this->checkCache()) {
            $this->templateContent = $this->loadFile($this->templateFile);
            if ($extendFile = $this->hasExtend()) {
                $baseFilePath          = $extendFile;
                $listen[$baseFilePath] = time();
                $this->parseExtend($baseFilePath);
            }
            $cache = $this->parseTemplate();

            $listen[$this->templateFile] = time();

            $cacheInfo = serialize($listen);
            $headInfo  = '<?php /*' . $cacheInfo . '*/' . PHP_EOL . 'if(!defined(\'APP\')){die(\'error!this is a cache file!\');}?>';
            if (!file_put_contents($this->compileFile, $headInfo . $cache)) {
                throw new BaseException($this->compileFile . ":Cache write failed!" . $cache);
            };
        };
        //执行编译后的文件s
        //单独放置一个函数 是为了不让控制器的变量和内容的变量冲突
        $this->loadCacheFile();
    }


    private function parseTemplatePath($template)
    {
        return ROOT_PATH . DIRECTORY_SEPARATOR . APP . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR . $template . '.html';
    }

    private function parseCachePath($template)
    {
        $template = str_replace("/", "_", $template);
        $template = str_replace("\\", "_", $template);

        $cacheFile = ROOT_PATH . DIRECTORY_SEPARATOR . APP . DIRECTORY_SEPARATOR .
            'runtime' . DIRECTORY_SEPARATOR .
            'tpl_cache';

        if (!is_dir($cacheFile)) {
            mkdir($cacheFile);
        }

        return $cacheFile . DIRECTORY_SEPARATOR . $template . '.php';
    }

    private function hasExtend()
    {
        $m       = [];
        $pattern = '#^' . $this->leftLimit . 'extend\s*name=([\"|\'])([^\\1]+?)\\1' . $this->rightLimit . '#i';
        if (preg_match($pattern, $this->templateContent, $m)) {
            return $this->parseTemplatePath($m[2]);
        }
        return false;
    }

    /**
     * @param $extendFile
     * @throws BaseException
     */
    private function parseExtend($extendFile)
    {
        $str     = $this->templateContent;
        $blocks  = [];
        $pattern = '/' . $this->leftLimit . 'block\s*name=\"(?P<name>[a-zA-Z0-9_]*)\"' .
            $this->rightLimit . '\s*(?P<fragment>.*?)\s*' . $this->leftLimit .
            '\/block' . $this->rightLimit . '/is';

        preg_match_all($pattern, $str, $m, PREG_SET_ORDER);
        foreach ($m as $match) {
            $blocks[$match['name']] = $match['fragment'];
        }

        $baseContent           = $this->loadFile($extendFile);
        $this->templateContent = preg_replace_callback($pattern, function ($m) use ($blocks) {
            return $blocks[$m['name']] ?? $m['fragment'];
        }, $baseContent);
    }

    protected function parseVarFilter($varName, $filters): string
    {
        $varName = '$' . $varName;
        if (!$filters) {
            return '$this->filter(' . $varName . ')';
        }
        $default_filter = false;
        //$first = array_shift($filters);
        if ($filters[0] != 'raw') {
            $default_filter = true;
        } else {
            array_shift($filters);
        }
        foreach ($filters as $filter) {
            switch ($filter) {
                case "date":
                    $varName = 'date("Y/m/d H:i:s",' . $varName . ')';
                    break;
                default:
                    $varName = $filter . '(' . $varName . ')';
                    break;
            }
        }
        if ($default_filter) {
            $varName = '$this->filter(' . $varName . ')';
        }
        return $varName;
    }

    /**
     * @param $var
     * @return string
     * @noinspection PhpUnused
     */
    public function filter($var): string
    {
        return htmlspecialchars($var, ENT_QUOTES);
    }

    /**
     * 解析模板
     * @return string
     */
    private function parseTemplate(): string
    {
        $pattern = $this->convertIntoPreg(array_keys($this->tags));
        preg_match_all($pattern, $this->templateContent,
            $this->matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
        return $this->parseTags(false);

    }


    /**
     * 解析标签属性
     * @param $tagName
     * @param $expression
     * @return array|string
     */
    protected function parseTagAttrs($tagName, $expression)
    {
        $tag        = $this->tags[$tagName];
        $expression = trim($expression);
        if (!isset($tag['expression']) || !$tag['expression']) {
            $p     = '#(?P<name>[^=]+)\s*=(?>\s*)(["|\'])(?P<value>[^\2]+?)\2#i';
            $attrs = [];
            if (preg_match_all($p, $expression, $matches, PREG_SET_ORDER)) {
                foreach ($matches as $match) {
                    $attrs[$match['name']] = $match['value'];
                }
            }
            return $attrs;
        }
        return $expression;
    }

    /**
     * 解析标签
     * @param $findTag
     * @return string
     */
    private function parseTags($findTag)
    {
        $out = '';
        while ($match = array_shift($this->matches)) {

            $prefixSymbol = $match[1][0] ?? ''; //简写符号 例如:和$
            $sampleTag    = $match[2][0] ?? ''; //简写符号后面的标签
            $closeSymbol  = $match[3][0] ?? '';//关闭符号也就是/
            $name         = $match[4][0] ?? '';//标签的名字
            $offset       = $match[0][1] ?? ''; //偏移量


            $offsetEnd = $offset + strlen($match[0][0]);

            if ($closeSymbol === "") {
                $expression = $match[5][0] ?? '';
                $out        .= $this->getHtmlFragment($offset - $this->position);

                $this->position = $offsetEnd;

                //判断是否是简写标签，如果是则获取对应的完整标签
                if ($prefixSymbol) {
                    if (isset($this->sampleTags[$prefixSymbol])) {
                        $name       = $this->sampleTags[$prefixSymbol];
                        $expression = $sampleTag;
                    }
                }
                //构建标签处理函数
                $handler = 'parse' . ucfirst($name) . 'Tag';
                $tag     = $this->tags[$name];

                if (isset($tag['close']) && $tag['close']) {
                    //如果设置为需要闭合标签， 则递归查找闭合标签。
                    $html = $this->parseTags(["name" => $name, "expression" => $expression]);
                    //解析表达式
                    $expression = $this->parseTagAttrs($name, $expression);
                    $out        .= call_user_func_array([$this, $handler], [$expression, $html]);
                    continue;

                }
                //不需要闭合标签的单行标签
                $expression = $this->parseTagAttrs($name, $expression);
                $out        .= call_user_func_array([$this, $handler], [$expression]);
                continue;

            } else {
                $name = ltrim($name, '/');
                if (!$findTag)
                    throw new TemplateException("错误的结束标签" . $name);

                if ($name == $findTag['name']) {
                    $out .= $this->getHtmlFragment($offset - $this->position);

                    $this->position = $offsetEnd;
                    return $out;
                }
            }
        }
        if ($findTag) {
            throw new TemplateException("标签未闭合" . $findTag['name']);
        }
        //最后还得收个尾
        $out .= $this->getHtmlFragment();
        return $out;
    }

    /**
     * 获取html片段
     * @param null $len
     * @return false|string
     */
    protected function getHtmlFragment($len = null): bool|string
    {
        if ($len === null)
            return substr($this->templateContent, $this->position);
        return substr($this->templateContent, $this->position, $len);
    }

    /**
     * 标签转换为正则表达式
     * @param $tags
     * @return string
     */
    private function convertIntoPreg($tags): string
    {
        $begin  = $this->leftLimit;
        $end    = $this->rightLimit;
        $tagStr = implode("|", $tags);
        return '#' . $begin . '(?:([\:|\$])([^' . $this->end . ']+)|(\/?)([^\s\}]+)(\s+[^' . $this->end . ']+)?)' . $end . '#is';
    }


    private function checkCache(): bool
    {
        if (!file_exists($this->compileFile)) {
            return false;
        }
        // 读取缓存文件失败
        if (!$handle = fopen($this->compileFile, "r")) {
            return false;
        }

        // 读取第一行
        if (!preg_match('/\/\*([^*]+)\*\//', fgets($handle), $matches)) {
            fclose($handle);
            return false;
        }
        fclose($handle);

        $includeFile = unserialize($matches[1]);
        if (!is_array($includeFile)) {
            return false;
        }

        // 检查模板文件是否有更新
        foreach ($includeFile as $path => $time) {
            if (!is_file($path) || filemtime($path) > $time) {
                // 模板文件如果有更新则缓存需要更新
                return false;
            }
        }
        return true;
    }

    /**
     * @param $file
     * @return bool|string
     */
    private function loadFile($file): bool|string
    {
        if (!is_file($file)) {
            throw new TemplateException("文件不存在" . $file);
        }
        return file_get_contents($file);
    }

    /**
     *
     */
    private function loadCacheFile()
    {
        extract($this->vars, EXTR_REFS);
        if (function_exists('opcache_invalidate')) {
            opcache_invalidate($this->compileFile, true);
        }
        include($this->compileFile);
    }


    /**
     * 给模板内容赋值
     * @param $var
     * @param mixed $value
     */
    public function set($var, mixed $value = '')
    {
        $this->vars[$var] = $value;
    }
}